/*
 */
package aptech.eproject.logictics.dao.kien;

import aptech.eproject.logictics.db.BookingStatus;
import aptech.eproject.logictics.db.Branch;
import aptech.eproject.logictics.db.Delivery;
import aptech.eproject.logictics.db.DeliveryStatus;
import aptech.eproject.logictics.db.Package;
import aptech.eproject.logictics.db.Warehouse;
import aptech.eproject.logictics.db.WarehousePK;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.List;
import javax.ejb.Stateless;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import aptech.eproject.logictics.db.Customer;
import java.util.Collection;
import java.util.Set;
import javax.annotation.Resource;
import javax.ejb.Timeout;
import javax.ejb.Timer;
import javax.ejb.TimerService;
import javax.persistence.EntityNotFoundException;
import aptech.eproject.logictics.dao.Util;
import aptech.eproject.logictics.dao.thinh.RouteManageRemote;
import aptech.eproject.logictics.db.Route;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * Stateless session bean, provides methods for Employee's
 * Booking requests management like: <em>accept</em>,
 * <em>reject</em>, <em>pickUp</em>,
 * <em>getBookingList</em>, customer's <em>confirm</em>
 * and some rate calculation
 * @author Kien Nguyen <solidfoxrock at gmail.com>
 */
@Stateless
public class BookingManagementService implements BookingManagementServiceRemote {

    @PersistenceContext
    private EntityManager em;
    @Resource
    private TimerService timerService;

    /**
     * Return all bookings in database (which is not deleted)
     * @return list of {@link Delivery} instance
     */
    @Override
    public List<Delivery> findAllBookings() {
        Query q = em.createNamedQuery("findAllDeliveries");
        return q.getResultList();
    }

    /**
     * Return all bookings of the branch with provided id
     * @param branchId
     * @return list of {@link Delivery} instance
     */
    @Override
    public List<Delivery> findBookings(int branchId) {
        Query q = em.createNamedQuery("findDeliveries");
        q.setParameter(1, branchId);
        return q.getResultList();
    }

    /**
     * Get one booking with {@code deliveryId}
     * @param deliveryId
     * @param branchId
     * @return a {@link Delivery} instance if exists, null otherwise
     */
    @Override
    public Delivery findBooking(int deliveryId, int branchId) {
        Query q = em.createNamedQuery("findDelivery");
        q.setParameter(1, branchId);
        q.setParameter(2, deliveryId);
        return (Delivery) q.getSingleResult();
    }

    /**
     * Employee accepts a booking request, update
     * booking status to <em>Accepted</em>
     * @param deliveryId
     * @throws EntityNotFoundException
     *          if {@code deliveryId} not exists
     * @throws IllegalStateException
     *          if delivery's BookingStatus is not <em>Pending</em>
     */
    @Override
    public void accept(int deliveryId) {
        Delivery delivery = em.find(Delivery.class, deliveryId);

        // if booking not existed
        if (delivery == null) {
            throw new EntityNotFoundException("Booking with id " + deliveryId + " not found");
        }

        // if booking status is not Pending
        if (delivery.getBookingStatus() != BookingStatus.Pending) {
            throw new IllegalStateException("Booking status must be 'Pending' in order to be accepted");
        }

        // update booking status
        delivery.setBookingStatus(BookingStatus.Accepted);
        em.merge(delivery);
    }

    /**
     * Employee or system rejects a booking request, update
     * booking status to <em>Rejected</em>
     * @param deliveryId
     * @throws EntityNotFoundException
     *          if {@code deliveryId} not exists
     * @throws IllegalStateException
     *          if delivery's BookingStatus is not <i>Pending</i>
     */
    @Override
    public void reject(int deliveryId) {

        Delivery delivery = em.find(Delivery.class, deliveryId);

        // if booking not existed
        if (delivery == null) {
            throw new EntityNotFoundException("Booking with id " + deliveryId + " not found");
        }

        // if booking status is not Pending
        if (delivery.getBookingStatus() != BookingStatus.Pending) {
            throw new IllegalStateException("Booking status must be 'Pending' in order to be rejected");
        }

        // update booking status
        delivery.setBookingStatus(BookingStatus.Rejected);
        em.merge(delivery);
    }

    /**
     * Customer confirms a booking request, update booking status
     * to <em>Confirmed</em>, update booking's confirmedDate
     * to the current time
     * @param deliveryId
     * @throws EntityNotFoundException
     *          if {@code deliveryId} not exists
     * @throws IllegalStateException
     *          if delivery's BookingStatus is not <em>Accepted</em>
     */
    @Override
    public void confirm(int deliveryId) {

        Delivery delivery = em.find(Delivery.class, deliveryId);

        // if booking not existed
        if (delivery == null) {
            throw new EntityNotFoundException("Booking with id " + deliveryId + " not found");
        }

        // if booking status is not Pending
        if (delivery.getBookingStatus() != BookingStatus.Accepted) {
            throw new IllegalStateException("Booking status must be 'Accepted' in order to be confirmed");
        }

        // update booking status
        delivery.setBookingStatus(BookingStatus.Confirmed);
        // update booking's confirmedDate
        delivery.setConfirmedDate(new Timestamp(System.currentTimeMillis()));
        em.merge(delivery);
    }

    /**
     *
     * @param deliveryId
     */
    @Override
    public void cancel(int deliveryId) {

        Delivery delivery = em.find(Delivery.class, deliveryId);

        // if booking not existed
        if (delivery == null) {
            throw new EntityNotFoundException("Booking with id " + deliveryId + " not found");
        }

        BookingStatus bookingStatus = delivery.getBookingStatus();
        // if booking status is not Pending
        if (bookingStatus != BookingStatus.Pending && bookingStatus == BookingStatus.Accepted
                && bookingStatus == BookingStatus.Confirmed) {
            throw new IllegalStateException(
                    "Booking status must be 'Pending', 'Accepted' or 'Confirmed' in order to be rejected");
        }

        if (bookingStatus == BookingStatus.Confirmed) {
            // charge cancellation fee
            double cancelFee = calculateCancellationFee(delivery);
            delivery.setPubnishRate(new BigDecimal(cancelFee));
        }

        // if bookingStatus is Pending or Accepted, update status only:
        // update booking status
        delivery.setBookingStatus(BookingStatus.Canceled);
        em.merge(delivery);
    }

    /**
     * Calculate cancellation fee when customer
     * cancels a delivery after confirming
     * @param d
     *  Delivery entity
     * @return
     *  cancellation fee
     */
    private double calculateCancellationFee(Delivery d) {
        long mil = 86400000;
        double days = (System.currentTimeMillis() - d.getConfirmedDate().getTime()) / mil;
        double percent = 5;
        if (days <= 1) {
            //cancel within 1 day
            percent = 5;
        } else if (days <= 2) {
            //cancel within 2 days
            percent = 10;
        } else {
            //cancel in more than 2 days
            percent = 15;
        }
        double fee = percent / 100 * d.getRate().doubleValue();
        return fee;
    }

    /**
     * Employee picks up a booking's packages, update booking
     * status to <em>PickedUp</em>, insert all packages of
     * this delivery into {@link Warehouse} table
     * @param deliveryId
     * @throws EntityNotFoundException
     *          if {@code deliveryId} not exists
     * @throws IllegalStateException
     *          if delivery's BookingStatus is not <em>Confirmed</em>
     */
    @Override
    public void pickUp(int deliveryId) {

        Delivery delivery = em.find(Delivery.class, deliveryId);

        // if booking not existed
        if (delivery == null) {
            throw new EntityNotFoundException("Booking with id " + deliveryId + " not found");
        }

        // if booking status is not Confirmed
        if (delivery.getBookingStatus() != BookingStatus.Confirmed) {
            throw new IllegalStateException("Booking status must be 'Confirmed' in order to be picked up");
        }

        // update booking status
        delivery.setBookingStatus(BookingStatus.PickedUp);

        // getting start branch of the booking
        Branch branch = delivery.getStartBranch();

        // save all packages of booking into warehouse of branch
        Set<Package> packages = delivery.getPackages();
        for (Package p : packages) {

            // update package's Delivery Status to 'Waiting'
            p.setDeliveryStatus(DeliveryStatus.Waiting);

            // insert into Warehouse table
            Warehouse warehouse = new Warehouse(
                    new WarehousePK(branch.getBranchId(), p.getPackageId()), false);
            warehouse.setBranch(branch);
            warehouse.setPackage(p);
            em.persist(warehouse);
        }
    }

    /**
     * Get all detail rates of a {@link Delivery} as a List of rate
     * @param d a {@link Delivery} instance
     * @return A list contains every package rate of
     * the delivery, the last item is the total rate
     */
    @Override
    public List<Double> getDetailRates(Delivery d) {

        double totalRate = d.getRate().doubleValue();
        // total rate = total weight * unit price * distance
        // temp = unit price * distance
        double temp = totalRate / d.getTotalWeight();

        List<Double> rates = new ArrayList<Double>();

        // calculate all package detail rate
        Set<Package> packages = d.getPackages();
        for (Package p : packages) {
            double packageRate = p.getWeight() * temp;
            rates.add(packageRate);
        }
        // add totalRate to last of list
        rates.add(totalRate);
        return rates;
    }

    /**
     * Calculate and return total rate of a {@link Delivery} instance
     * @param d a {@link Delivery} instance
     * @return total rate
     */
    @Override
    public double calculateTotalRate(Delivery d) {

        double distance = findShortestDistance(
                d.getStartBranch().getBranchId(),
                d.getDestinationBranch().getBranchId());
        double unitPrice = d.getStartBranch().getUnitPrice().doubleValue();

        return d.getTotalWeight() * distance * unitPrice;
    }

    /**
     * Calculate and return commission rate of a {@link Delivery} instance
     * @param d a {@link Delivery} instance
     * @return comission rate, 0 if <tt>d</tt> does not have a total rate
     */
    @Override
    public double calculateCommission(Delivery d) {
        BigDecimal rate = d.getRate();
        if (rate == null) {
            return 0;
        }
        double commissionPercentage = d.getStartBranch().getCommissionPercent();
        return rate.doubleValue() * commissionPercentage * 0.01;
    }
    int n;//Number of branches
    double[][] aMTK;//Represent the map of branches, used in dijkstra algobrithm
    int[] s;//Used in dijkstra algobrithm
    int[] truoc;//Used in dijkstra algobrithm
    int[] mark;//Used in dijkstra algobrithm
    double[] dodai;//Used in dijkstra algobrithm
    int count = 0;//Used in dijkstra algobrithm

    private void initdijkstra()//initialize for dijkstra algobrithm
    {
        for (int i = 1; i <= n; i++) {
            dodai[i] = 1000;
            mark[i] = 0;
            truoc[i] = 0;
            s[i] = 0;
        }
    }

    private int minimum()//a part of dijkstra algobrithm
    {
        double min = 1000;
        int i;
        int t = 1;
        for (i = 1; i <= n; i++) {
            if (mark[i] != 1)//neu chua xet dinh i;
            {
                if (min >= dodai[i])//neu dodai[i] nho hon min
                {
                    min = dodai[i];//min bang do dai cua dodai[i]
                    t = i;//t bang dinh i
                }
            }
        }
        return t;

    }

    private void dijkstra()//dijkstra algobrithm to find the shortest distance between two branches
    {
        int i;
        int j;
        int u;
        count = 0;
        while (count < n) {
            u = minimum();
            s[++count] = u;

            mark[u] = 1;
            for (i = 1; i <= n; i++) {
                if (aMTK[u][i] != 0)//if there is a route between branch u and i
                {
                    if (dodai[i] > dodai[u] + aMTK[u][i]) {
                        dodai[i] = dodai[u] + aMTK[u][i];
                        truoc[i] = u;
                    }
                }
            }
        }
    }

    /**
     * Finds the shortest distance between two branches using dijkstra algobrithm
     * @param startBranchId id of the first Branch
     * @param destBranchId id of the second Branch
     * @return shortest distance between two branches
     */
    @Override
    public double findShortestDistance(
            int startBranchId,
            int destBranchId) {
        //Load route list:        
        RouteManageRemote rRemote = Util.lookupRouteService();
        BranchServiceRemote bRemote = Util.lookupBranchService();
        List lstRoute = rRemote.getListRoute();
        List lstBranch = bRemote.findBranches();
        int i;
        n = lstBranch.size();//Number of branches
        Route r;
        int sBranchID;
        int dBranchID;
        n++;
        aMTK = new double[n][n];
        s = new int[n];
        truoc = new int[n];
        mark = new int[n];
        dodai = new double[n];
        n = n - 1;
        for (i = 0; i < lstRoute.size(); i++) {
            r = (Route) lstRoute.get(i);
            sBranchID = r.getFromBranch().getBranchId();
            dBranchID = r.getToBranch().getBranchId();
            aMTK[sBranchID][dBranchID] = r.getDistance();
        }
        initdijkstra();
        dodai[startBranchId] = 0;
        dijkstra();
        double result = dodai[destBranchId];
        if (result == 0 || result < 1) {
            result = 10;
        }
        return result;
        //return 300;
    }

    /**
     *
     */
    @Override
    public void checkBookings() {

        // check Pending bookings
        Query q = em.createNamedQuery("findDeliveriesByBookingStatus");
        q.setParameter(1, BookingStatus.Pending);
        List<Delivery> bookings = q.getResultList();
        long mil = 86400000;
        Timestamp bookedDate;
        double days;
        for (Delivery booking : bookings) {
            bookedDate = booking.getBookedDate();
            days = ((double) System.currentTimeMillis() - bookedDate.getTime()) / mil;
            // if the booking has been pending for over 2 days, reject it
            if (days > 2) {
                reject(booking.getDeliveryId());
                System.out.println("Rejected booking with id " + booking.getDeliveryId());
            }
        }

        // check Accepted bookings
        q = em.createNamedQuery("findDeliveriesByBookingStatus");
        q.setParameter(1, BookingStatus.Accepted);
        bookings = q.getResultList();
        for (Delivery booking : bookings) {
            bookedDate = booking.getBookedDate();
            days = ((double) System.currentTimeMillis() - bookedDate.getTime()) / mil;
            // if the booking has been accepted but was not confirmed
            // after 4 days since the booked date, cancel it
            if (days > 4) {
                cancel(booking.getDeliveryId());
                System.out.println("Canceled booking with id " + booking.getDeliveryId());
            }
        }
    }

    /**
     *
     * @param timer
     */
    @Override
    @Timeout
    public void agentCallBack(
            Timer timer) {
        System.out.println("Logistics Agent is checking. ["
                + new SimpleDateFormat("MMM dd, hh:mm:ss").format(new Date()) + "]");
        checkBookings();
    }

    /**
     *
     */
    @Override
    public void startAgent() {
        Collection<Timer> timers = timerService.getTimers();
        if (timers.isEmpty()) {
            timerService.createTimer(120000, 120000, "Logistics Agent");
//            System.out.println("Logistics Agent has started");
        } else {
//            System.out.println("Logistics Agent is still running");
        }
    }

    /**
     *
     */
    @Override
    public void stopAgent() {
        Collection<Timer> timers = timerService.getTimers();
        if (timers.isEmpty()) {
//            System.out.println("Logistics Agent is not running");
        } else {
            for (Timer timer : timers) {
                timer.cancel();
//                System.out.println("Logistics Agent has stopped");
            }
        }
    }

    /**
     *
     * @param userName
     * @param startBranchId
     * @param destBranchId
     * @param pickUpAddress
     * @param consigneeName
     * @param consigneeAddress
     * @param pickUpDate
     * @param packages
     * @return
     */
    @Override
    public int createBooking(String userName, int startBranchId, int destBranchId,
            String pickUpAddress,
            String consigneeName, String consigneeAddress,
            Timestamp pickUpDate, List<Package> packages) {

        Delivery delivery = new Delivery();

        Customer customer = em.find(Customer.class, userName);
        delivery.setConsigner(customer);

        Branch startBranch = em.find(Branch.class, startBranchId);
        delivery.setStartBranch(startBranch);

        Branch destBranch = em.find(Branch.class, destBranchId);
        delivery.setDestinationBranch(destBranch);

        delivery.setPickUpAddress(pickUpAddress);
        delivery.setConsigneeName(consigneeName);
        delivery.setConsigneeAddress(consigneeAddress);
        delivery.setPickupDate(pickUpDate);

        double totalWeight = 0;
        for (Package p : packages) {
            totalWeight += p.getWeight();
        }
        delivery.setTotalWeight(totalWeight);

        delivery.setRate(BigDecimal.valueOf(calculateTotalRate(delivery)));
        delivery.setComission(BigDecimal.valueOf(calculateCommission(delivery)));

        delivery.setBookingStatus(BookingStatus.Pending);
        delivery.setBookedDate(new Timestamp(System.currentTimeMillis()));

        em.persist(delivery);

        for (Package p : packages) {
            p.setDelivery(delivery);
            p.setDeliveryStatus(DeliveryStatus.Planned);
            em.persist(p);
        }

        return delivery.getDeliveryId();
    }

    /**
     *
     * @param deliveryId
     * @param ccNum
     */
    @Override
    public void saveCreditCardNumber(int deliveryId, String ccNum) {
        Delivery delivery = em.find(Delivery.class, deliveryId);
        if (delivery == null) {
            throw new EntityNotFoundException(
                    "Delivery with id " + deliveryId + " not found, this should never happens");
        }
        delivery.setCustomerccNumber(ccNum);
        em.merge(delivery);
    }
}
